<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP version 4.0													  |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2004 The PHP Group								|
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license,	   |
// | that is bundled with this package in the file LICENSE, and is		|
// | available at through the world-wide-web at						   |
// | http://www.php.net/license/2_02.txt.								 |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to		  |
// | license@php.net so we can mail you a copy immediately.			   |
// +----------------------------------------------------------------------+
// | Authors: Herim Vasquez <vasquezh@iro.umontreal.ca>				   |
// |		  Bertrand Mansion <bmansion@mamasam.com>					 |
// |		  Alexey Borzov <avb@php.net>
// +----------------------------------------------------------------------+
//
// $Id: hierselect.php,v 1.1 2006/09/24 17:04:55 jamiesensei Exp $

require_once('HTML/QuickForm/group.php');
require_once('HTML/QuickForm/select.php');

/**
 * Class to dynamically create two or more HTML Select elements
 * The first select changes the content of the second select and so on.
 * This element is considered as a group. Selects will be named
 * groupName[0], groupName[1], groupName[2]...
 *
 * @author	   Herim Vasquez <vasquezh@iro.umontreal.ca>
 * @author	   Bertrand Mansion <bmansion@mamasam.com>
 * @version	  1.0
 * @since		PHP4.04pl1
 * @access	   public
 */
class HTML_QuickForm_hierselect extends HTML_QuickForm_group
{   
	// {{{ properties

	/**
	 * Options for all the select elements
	 *
	 * Format is a bit more complex as we need to know which options
	 * are related to the ones in the previous select:
	 *
	 * Ex:
	 * // first select
	 * $select1[0] = 'Pop';
	 * $select1[1] = 'Classical';
	 * $select1[2] = 'Funeral doom';
	 *
	 * // second select
	 * $select2[0][0] = 'Red Hot Chil Peppers';
	 * $select2[0][1] = 'The Pixies';
	 * $select2[1][0] = 'Wagner';
	 * $select2[1][1] = 'Strauss';
	 * $select2[2][0] = 'Pantheist';
	 * $select2[2][1] = 'Skepticism';
	 *
	 * // If only need two selects 
	 * //	 - and using the depracated functions
	 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
	 * $sel->setMainOptions($select1);
	 * $sel->setSecOptions($select2);
	 *
	 * //	 - and using the new setOptions function
	 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
	 * $sel->setOptions(array($select1, $select2));
	 *
	 * // If you have a third select with prices for the cds
	 * $select3[0][0][0] = '15.00$';
	 * $select3[0][0][1] = '17.00$';
	 * etc
	 *
	 * // You can now use
	 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
	 * $sel->setOptions(array($select1, $select2, $select3));
	 * 
	 * @var	   array
	 * @access	private
	 */
	var $_options = array();
	
	/**
	 * Number of select elements on this group
	 *
	 * @var	   int
	 * @access	private
	 */
	var $_nbElements = 0;

	/**
	 * The javascript used to set and change the options
	 *
	 * @var	   string
	 * @access	private
	 */
	var $_js = '';

	// }}}
	// {{{ constructor

	/**
	 * Class constructor
	 * 
	 * @param	 string	$elementName	(optional)Input field name attribute
	 * @param	 string	$elementLabel   (optional)Input field label in form
	 * @param	 mixed	 $attributes	 (optional)Either a typical HTML attribute string 
	 *									  or an associative array. Date format is passed along the attributes.
	 * @param	 mixed	 $separator	  (optional)Use a string for one separator,
	 *									  use an array to alternate the separators.
	 * @access	public
	 * @return	void
	 */
	function HTML_QuickForm_hierselect($elementName=null, $elementLabel=null, $attributes=null, $separator=null)
	{
		$this->HTML_QuickForm_element($elementName, $elementLabel, $attributes);
		$this->_persistantFreeze = true;
		if (isset($separator)) {
			$this->_separator = $separator;
		}
		$this->_type = 'hierselect';
		$this->_appendName = true;
	} //end constructor

	// }}}
	// {{{ setOptions()

	/**
	 * Initialize the array structure containing the options for each select element.
	 * Call the functions that actually do the magic.
	 *
	 * @param	 array	$options	Array of options defining each element
	 *
	 * @access	public
	 * @return	void
	 */
	function setOptions($options)
	{
		$this->_options = $options;

		if (empty($this->_elements)) {
			$this->_nbElements = count($this->_options);
			$this->_createElements();
		} else {
			// setDefaults has probably been called before this function
			// check if all elements have been created
			$totalNbElements = count($this->_options);
			for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) {
				$this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
				$this->_nbElements++;
			}
		}
		
		$this->_setOptions();
	} // end func setMainOptions

	// }}}
	// {{{ setMainOptions()
	
	/**
	 * Sets the options for the first select element. Deprecated. setOptions() should be used.
	 *
	 * @param	 array	 $array	Options for the first select element
	 *
	 * @access	public
	 * @deprecated		  Deprecated since release 3.2.2
	 * @return	void
	 */
	function setMainOptions($array)
	{
		$this->_options[0] = $array;

		if (empty($this->_elements)) {
			$this->_nbElements = 2;
			$this->_createElements();
		}
	} // end func setMainOptions
	
	// }}}
	// {{{ setSecOptions()
	
	/**
	 * Sets the options for the second select element. Deprecated. setOptions() should be used.
	 * The main _options array is initialized and the _setOptions function is called.
	 *
	 * @param	 array	 $array	Options for the second select element
	 *
	 * @access	public
	 * @deprecated		  Deprecated since release 3.2.2
	 * @return	void
	 */
	function setSecOptions($array)
	{
		$this->_options[1] = $array;

		if (empty($this->_elements)) {
			$this->_nbElements = 2;
			$this->_createElements();
		} else {
			// setDefaults has probably been called before this function
			// check if all elements have been created
			$totalNbElements = 2;
			for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) {
				$this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
				$this->_nbElements++;
			}
		}
		
		$this->_setOptions();
	} // end func setSecOptions
	
	// }}}
	// {{{ _setOptions()
	
	/**
	 * Sets the options for each select element
	 *
	 * @access	private
	 * @return	void
	 */
	function _setOptions()
	{
		$toLoad = '';
		foreach (array_keys($this->_elements) AS $key) {
			$array = eval("return isset(\$this->_options[{$key}]{$toLoad})? \$this->_options[{$key}]{$toLoad}: null;");
			if (is_array($array)) {
				$select =& $this->_elements[$key];
				$select->_options = array();
				$select->loadArray($array);

				$value  = is_array($v = $select->getValue()) ? $v[0] : key($array);
				$toLoad .= '[\'' . str_replace(array('\\', '\''), array('\\\\', '\\\''), $value) . '\']';
			}
		}
	} // end func _setOptions
	
	// }}}
	// {{{ setValue()

	/**
	 * Sets values for group's elements
	 * 
	 * @param	 array	 $value	An array of 2 or more values, for the first,
	 *								the second, the third etc. select
	 *
	 * @access	public
	 * @return	void
	 */
	function setValue($value)
	{
		// fix for bug #6766. Hope this doesn't break anything more 
		// after bug #7961. Forgot that _nbElements was used in
		// _createElements() called in several places... 
		$this->_nbElements = max($this->_nbElements, count($value));
		parent::setValue($value);
		$this->_setOptions();
	} // end func setValue
	
	// }}}
	// {{{ _createElements()

	/**
	 * Creates all the elements for the group
	 * 
	 * @access	private
	 * @return	void
	 */
	function _createElements()
	{
		for ($i = 0; $i < $this->_nbElements; $i++) {
			$this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
		}
	} // end func _createElements

	// }}}
	// {{{ toHtml()

	function toHtml()
	{
		$this->_js = '';
		if (!$this->_flagFrozen) {
			// set the onchange attribute for each element except last
			$keys	 = array_keys($this->_elements);
			$onChange = array();
			for ($i = 0; $i < count($keys) - 1; $i++) {
				$select =& $this->_elements[$keys[$i]];
				$onChange[$i] = $select->getAttribute('onchange');
				$select->updateAttributes(
					array('onchange' => '_hs_swapOptions(this.form, \'' . $this->_escapeString($this->getName()) . '\', ' . $keys[$i] . ');' . $onChange[$i])
				);
			}
			
			// create the js function to call
			if (!defined('HTML_QUICKFORM_HIERSELECT_EXISTS')) {
				$this->_js .= <<<JAVASCRIPT
function _hs_findOptions(ary, keys)
{
	var key = keys.shift();
	if (!key in ary) {
		return {};
	} else if (0 == keys.length) {
		return ary[key];
	} else {
		return _hs_findOptions(ary[key], keys);
	}
}

function _hs_findSelect(form, groupName, selectIndex)
{
	if (groupName+'['+ selectIndex +']' in form) {
		return form[groupName+'['+ selectIndex +']']; 
	} else {
		return form[groupName+'['+ selectIndex +'][]']; 
	}
}

function _hs_unescapeEntities(str)
{
	var div = document.createElement('div');
	div.innerHTML = str;
	return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
}

function _hs_replaceOptions(ctl, optionList)
{
	var j = 0;
	ctl.options.length = 0;
	for (i in optionList) {
		var optionText = (-1 == optionList[i].indexOf('&'))? optionList[i]: _hs_unescapeEntities(optionList[i]);
		ctl.options[j++] = new Option(optionText, i, false, false);
	}
}

function _hs_setValue(ctl, value)
{
	var testValue = {};
	if (value instanceof Array) {
		for (var i = 0; i < value.length; i++) {
			testValue[value[i]] = true;
		}
	} else {
		testValue[value] = true;
	}
	for (var i = 0; i < ctl.options.length; i++) {
		if (ctl.options[i].value in testValue) {
			ctl.options[i].selected = true;
		}
	}
}

function _hs_swapOptions(form, groupName, selectIndex)
{
	var hsValue = [];
	for (var i = 0; i <= selectIndex; i++) {
		hsValue[i] = _hs_findSelect(form, groupName, i).value;
	}

	_hs_replaceOptions(_hs_findSelect(form, groupName, selectIndex + 1), 
					   _hs_findOptions(_hs_options[groupName][selectIndex], hsValue));
	if (selectIndex + 1 < _hs_options[groupName].length) {
		_hs_swapOptions(form, groupName, selectIndex + 1);
	}
}

function _hs_onReset(form, groupNames)
{
	for (var i = 0; i < groupNames.length; i++) {
		try {
			for (var j = 0; j <= _hs_options[groupNames[i]].length; j++) {
				_hs_setValue(_hs_findSelect(form, groupNames[i], j), _hs_defaults[groupNames[i]][j]);
				if (j < _hs_options[groupNames[i]].length) {
					_hs_replaceOptions(_hs_findSelect(form, groupNames[i], j + 1), 
									   _hs_findOptions(_hs_options[groupNames[i]][j], _hs_defaults[groupNames[i]].slice(0, j + 1)));
				}
			}
		} catch (e) {
			if (!(e instanceof TypeError)) {
				throw e;
			}
		}
	}
}

function _hs_setupOnReset(form, groupNames)
{
	setTimeout(function() { _hs_onReset(form, groupNames); }, 25);
}

function _hs_onReload()
{
	var ctl;
	for (var i = 0; i < document.forms.length; i++) {
		for (var j in _hs_defaults) {
			if (ctl = _hs_findSelect(document.forms[i], j, 0)) {
				for (var k = 0; k < _hs_defaults[j].length; k++) {
					_hs_setValue(_hs_findSelect(document.forms[i], j, k), _hs_defaults[j][k]);
				}
			}
		}
	}

	if (_hs_prevOnload) {
		_hs_prevOnload();
	}
}

var _hs_prevOnload = null;
if (window.onload) {
	_hs_prevOnload = window.onload;
}
window.onload = _hs_onReload;

var _hs_options = {};
var _hs_defaults = {};

JAVASCRIPT;
				define('HTML_QUICKFORM_HIERSELECT_EXISTS', true);
			}
			// option lists
			$jsParts = array();
			for ($i = 1; $i < $this->_nbElements; $i++) {
				$jsParts[] = $this->_convertArrayToJavascript($this->_options[$i]);
			}
			$this->_js .= "\n_hs_options['" . $this->_escapeString($this->getName()) . "'] = [\n" .
						  implode(",\n", $jsParts) .
						  "\n];\n";
			// default value; if we don't actually have any values yet just use
			// the first option (for single selects) or empty array (for multiple)
			$values = array();
			foreach (array_keys($this->_elements) as $key) {
				if (is_array($v = $this->_elements[$key]->getValue())) {
					$values[] = count($v) > 1? $v: $v[0];
				} else {
					// XXX: accessing the supposedly private _options array
					$values[] = $this->_elements[$key]->getMultiple() || empty($this->_elements[$key]->_options[0])?
								array():
								$this->_elements[$key]->_options[0]['attr']['value'];
				}
			}
			$this->_js .= "_hs_defaults['" . $this->_escapeString($this->getName()) . "'] = " .
						  $this->_convertArrayToJavascript($values, false) . ";\n";
		}
		include_once('HTML/QuickForm/Renderer/Default.php');
		$renderer =& new HTML_QuickForm_Renderer_Default();
		$renderer->setElementTemplate('{element}');
		parent::accept($renderer);

		if (!empty($onChange)) {
			$keys	 = array_keys($this->_elements);
			for ($i = 0; $i < count($keys) - 1; $i++) {
				$this->_elements[$keys[$i]]->updateAttributes(array('onchange' => $onChange[$i]));
			}
		}
		return (empty($this->_js)? '': "<script type=\"text/javascript\">\n//<![CDATA[\n" . $this->_js . "//]]>\n</script>") .
			   $renderer->toHtml();
	} // end func toHtml

	// }}}
	// {{{ accept()

	function accept(&$renderer, $required = false, $error = null)
	{
		$renderer->renderElement($this, $required, $error);
	} // end func accept

	// }}}
	// {{{ onQuickFormEvent()

	function onQuickFormEvent($event, $arg, &$caller)
	{
		if ('updateValue' == $event) {
			// we need to call setValue() so that the secondary option
			// matches the main option
			return HTML_QuickForm_element::onQuickFormEvent($event, $arg, $caller);
		} else {
			$ret = parent::onQuickFormEvent($event, $arg, $caller);
			// add onreset handler to form to properly reset hierselect (see bug #2970)
			if ('addElement' == $event) {
				$onReset = $caller->getAttribute('onreset');
				if (strlen($onReset)) {
					if (strpos($onReset, '_hs_setupOnReset')) {
						$caller->updateAttributes(array('onreset' => str_replace('_hs_setupOnReset(this, [', "_hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "', ", $onReset)));
					} else {
						$caller->updateAttributes(array('onreset' => "var temp = function() { {$onReset} } ; if (!temp()) { return false; } ; if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } "));
					}
				} else {
					$caller->updateAttributes(array('onreset' => "if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } "));
				}
			}
			return $ret;
		}
	} // end func onQuickFormEvent

	// }}}
	// {{{ _convertArrayToJavascript()

   /**
	* Converts PHP array to its Javascript analog
	*
	* @access private
	* @param  array	 PHP array to convert
	* @param  bool	  Generate Javascript object literal (default, works like PHP's associative array) or array literal
	* @return string	Javascript representation of the value
	*/
	function _convertArrayToJavascript($array, $assoc = true)
	{
		if (!is_array($array)) {
			return $this->_convertScalarToJavascript($array);
		} else {
			$items = array();
			foreach ($array as $key => $val) {
				$item = $assoc? "'" . $this->_escapeString($key) . "': ": '';
				if (is_array($val)) {
					$item .= $this->_convertArrayToJavascript($val, $assoc);
				} else {
					$item .= $this->_convertScalarToJavascript($val);
				}
				$items[] = $item;
			}
		}
		$js = implode(', ', $items);
		return $assoc? '{ ' . $js . ' }': '[' . $js . ']';
	}
	
	// }}}
	// {{{ _convertScalarToJavascript()

   /**
	* Converts PHP's scalar value to its Javascript analog
	*
	* @access private
	* @param  mixed	 PHP value to convert
	* @return string	Javascript representation of the value
	*/
	function _convertScalarToJavascript($val)
	{
		if (is_bool($val)) {
			return $val ? 'true' : 'false';
		} elseif (is_int($val) || is_double($val)) {
			return $val;
		} elseif (is_string($val)) {
			return "'" . $this->_escapeString($val) . "'";
		} elseif (is_null($val)) {
			return 'null';
		} else {
			// don't bother
			return '{}';
		}
	}

	// }}}
	// {{{ _escapeString()

   /**
	* Quotes the string so that it can be used in Javascript string constants   
	*
	* @access private
	* @param  string
	* @return string
	*/
	function _escapeString($str)
	{
		return strtr($str,array(
			"\r"	=> '\r',
			"\n"	=> '\n',
			"\t"	=> '\t',
			"'"	 => "\\'",
			'"'	 => '\"',
			'\\'	=> '\\\\'
		));
	}

	// }}}
} // end class HTML_QuickForm_hierselect
?>